左值和右值
- 左值和右值
- 辣鸡的C++ PRIMER解释
- 左值(lvalue) : 是那些求值结果为对象或者函数的表达式,一个表示对象的非常量左值可以作为赋值运算符的左侧运算对象。
- 右值(rvalue) : 是指一种表达式,其结果是值而非值所在的位置。
- 参考博客:https://blog.csdn.net/xuwqiang1994/article/details/79924310
- 左值(lvalue) : lvalue(locator value)代表一个在内存中占有确定位置的对象(换句话说就是有一个地址)。
- 右值(rvalue) : rvalue通过排他性来定义,每个表达式不是lvalue就是rvalue。因此从上面的lvalue的定义,rvalue是不在内存中占有确定位置的表达式。
- 参考博客:https://blog.csdn.net/qq_24964575/article/details/52202989
- 左值可以当作右值使用,右值不能当左值用.
- 左值可以出现在=的任何一方,
- 出现在=右方的左值,是把左值当作右值使用,这是非常正常的.
- 右值,不能当作左值使用.
- 能够(单独)出现在=左方的是左值(实际是左右值,左值总是可以当作右值使用)
- 只能够出现在=右方的,是右值(纯粹的右值),不能当作左值使用.
- 左值代表地址单元,右值代表数据本身.
- 地址单元里的数据,是左值变量的右值的含义.
- 右值包括,纯数据,比如常数100,字符串常量”12345”等
- 以及地址单元中存储的数据.比如x ,这是左值当作右值使用代表的意义.
- 以及一些常变量(有确切地址的常量,可以象变量一样有个名字,其实就是变量,只是右值化了,不能当左值使用了)的含义.
- 左值代表存储器的一个单元.
- 这是一个物理概念,不仅仅是个地址(地址不过是个编号,是个数值而已),是个实实在在的东西.
- 右值代表数据,一个数值而已.
- 参考博客:https://www.zhihu.com/question/382300648
- 取地址符取到的是一个地址,没有实际存储在哪,所以是右值
- 如果是寄存器变量也没有地址,所以是右值
- 辣鸡的C++ PRIMER解释
使用decltype时,左值返回引用类型,右值不返回引用类型。
- 对于自增自减(++i,i++)
- ++i将对象本身作为左值返回
- i++将对象原始值作为右值返回
右值引用(c++)
- 定义 &&
右值引用只能引用到右值,不能引用到左值(const 例外)
左值持久
右值短暂
- 字面常量、临时对象
- 所引用的对象将要被销毁
- 该对象没有其他用户
引用右值的变量则是左值,不能再被右值引用
int &&rr1 = 42;//true
int &&rr2 = rr1;//error
- move函数:获得绑定在左值上的右值引用
- 调用move后,原变量除赋值和销毁外,不能再使用他
右值引用作用
- 右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
- 能够更简洁明确地定义泛型函数。
引用折叠
- X& &、X& &&、X&& & 可折叠成 X&
- X&& && 可折叠成 X&&
右值引用详解
- 参考:[c++11]我理解的右值引用、移动语义和完美转发
移动语义(Move Sementics)— 移动构造和移动赋值
- 对于如下场景
- 使用拷贝构造函数对string(“hello”)进行构造,会造成没有意义的资源申请和释放操作
- 而移动语义可以直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间
// 拷贝构造函数
MyString(const MyString& str) {
CCtor ++;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
// 移动构造函数
MyString(MyString&& str) noexcept
:m_data(str.m_data) {
MCtor ++;
str.m_data = nullptr; //不再指向之前的资源了
}
// 拷贝赋值函数 =号重载
MyString& operator=(const MyString& str){
CAsgn ++;
if (this == &str) // 避免自我赋值!!
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
// 移动赋值函数 =号重载
MyString& operator=(MyString&& str) noexcept{
MAsgn ++;
if (this == &str) // 避免自我赋值!!
return *this;
delete[] m_data;
m_data = str.m_data;
str.m_data = nullptr; //不再指向之前的资源了
return *this;
}
- 对于
String("hello")
它传入的参数是个临时对象,是右值,优先进入移动构造函数而不是拷贝构造函数- 拷贝构造函数是重新分配一块内存空间,将要拷贝的对象复制过来
- 因为对于构造的右值马上就会销毁,所以移动构造函数会将自己的指针指向内存中已有的资源.并将本来指向这个资源的指针置为nullptr
std::move()
- 对于一些生命周期短的局部左值,如果需要更充分的利用起来,可以使用std::move()将左值转换为右值,从而方便应用移动语义
- std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义
- 转换所有权,注意是转换,不发生深拷贝!
std::move()底层实现
template <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type &&>(t); }
- 1.对于C++的模板存在
通用引用
的特性,然后根据引用折叠,将右值经过 T&& 传递类型保持不变还是右值,而左值经过 T&& 变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。 - 2.然后根据
模板偏特化
,获取左值引用和右值引用的具体类型. - 3.利用static_cast<>进行强制类型转换,返回右值引用
universal references(通用引用) — 引用折叠
当右值引用和模板结合的时候.T&&并不一定表示右值引用,它可能是个左值引用又可能是个右值引用.
- 这时&&是一个未定义的引用类型,称为universal references
只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references。
template<typename T>
void f( T&& param); //这里T的类型需要推导,所以&&是一个 universal references
template<typename T>
class Test {
Test(Test&& rhs); //Test是一个特定的类型,不需要类型推导,所以&&表示右值引用
};
void f(Test&& param); //右值引用
//复杂一点
template<typename T>
void f(std::vector<T>&& param); //在调用这个函数之前,这个vector<T>中的推断类型
//已经确定了,所以调用f函数的时候没有类型推断了,所以是 右值引用
template<typename T>
void f(const T&& param); //右值引用
// universal references仅仅发生在 T&& 下面,任何一点附加条件都会使之失效
引用折叠
- 当
T
被推导成了string
,T&&
就是string&&
当
T
被推导成了string&
,T&&
就是string& &&
,这将会发生引用折叠规则如下:
- 所有的右值引用叠加到右值引用上仍然使一个右值引用。
- 有的其他引用类型之间的叠加都将变成左值引用。
完美转发
- 转发:就是通过一个函数将参数继续转交给另一个函数进行处理
- 完美:原参数可能是右值,可能是左值,转发后保持参数的原有特征
void process(int& i){
cout << "process(int&):" << i << endl;
}
void process(int&& i){
cout << "process(int&&):" << i << endl;
}
void myforward(int&& i){
cout << "myforward(int&&):" << i << endl;
process(i);
}
int main()
{
int a = 0;
process(a); //a被视为左值 process(int&):0
process(1); //1被视为右值 process(int&&):1
process(move(a)); //强制将a由左值改为右值 process(int&&):0
myforward(2); //右值经过forward函数转交给process函数,却称为了一个左值,
//原因是该右值有了名字 所以是 process(int&):2
myforward(move(a)); // 同上,在转发的时候右值变成了左值 process(int&):0
// myforward(a) // 错误用法,右值引用不接受左值
}
std::forward()
- 通过std::forward()可以完全转发右值
```cpp
void myforward(int&& i){
cout << “myforward(int&&):” << i << endl;
process(std::forward(i));
}
myforward(2); // process(int&&):2
* 对于同时解决左右值的完美转发,需要借助universal references通用引用类型和std::forward()模板函数共同实现完美转发.
```cpp
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
void RunCode(int &&m) {
cout << "rvalue ref" << endl;
}
void RunCode(int &m) {
cout << "lvalue ref" << endl;
}
void RunCode(const int &&m) {
cout << "const rvalue ref" << endl;
}
void RunCode(const int &m) {
cout << "const lvalue ref" << endl;
}
// 这里利用了universal references,如果写T&,就不支持传入右值,而写T&&,既能支持左值,又能支持右值
template<typename T>
void perfectForward(T && t) {
RunCode(forward<T> (t));
}
template<typename T>
void notPerfectForward(T && t) {
RunCode(t);
}